Skip to main content

Observer Pattern

Observer Pattern in JS

The Observer Pattern is one of the most widely used patterns in event-driven systems like Node.js.

Think of it like this:

"When something happens, notify everyone who cares about it."


What is Observer Pattern?

It is a behavioral design pattern where:

  • one object (the Subject/Publisher) maintains a list of dependents (Observers/Subscribers)
  • when the subject's state changes, it automatically notifies all observers

Real-world analogy

Think of a YouTube channel:

  • The channel is the subject
  • Subscribers are the observers
  • When a new video is uploaded, all subscribers get notified

You don't call each subscriber manually — they subscribed, and they get notified automatically.


Structure

It has 3 parts:

1. Subject (Publisher)

Maintains a list of observers and notifies them.

2. Observer (Subscriber)

Defines the update method that gets called.

3. Concrete Implementations

Actual subject and observer classes.


Manual Implementation in Plain JS

Problem: Order status change notification

When an order status changes, notify:

  • Email service
  • SMS service
  • Inventory service

Subject class

class OrderService {
#observers = [];

subscribe(observer) {
this.#observers.push(observer);
}

unsubscribe(observer) {
this.#observers = this.#observers.filter(o => o !== observer);
}

notify(event, data) {
this.#observers.forEach(observer => observer.update(event, data));
}

updateOrderStatus(orderId, status) {
console.log(`Order ${orderId} status updated to ${status}`);
this.notify('ORDER_STATUS_CHANGED', { orderId, status });
}
}

#observers uses a private class field — no one outside can tamper with the list.


Concrete observers

class EmailObserver {
update(event, data) {
if (event === 'ORDER_STATUS_CHANGED') {
console.log(`Email sent: Order ${data.orderId} is now ${data.status}`);
}
}
}

class SmsObserver {
update(event, data) {
if (event === 'ORDER_STATUS_CHANGED') {
console.log(`SMS sent: Order ${data.orderId} is now ${data.status}`);
}
}
}

class InventoryObserver {
update(event, data) {
if (event === 'ORDER_STATUS_CHANGED' && data.status === 'DELIVERED') {
console.log(`Inventory updated for order ${data.orderId}`);
}
}
}

Usage

const orderService = new OrderService();

const emailObserver = new EmailObserver();
const smsObserver = new SmsObserver();
const inventoryObserver = new InventoryObserver();

orderService.subscribe(emailObserver);
orderService.subscribe(smsObserver);
orderService.subscribe(inventoryObserver);

orderService.updateOrderStatus('ORD123', 'SHIPPED');
// Email sent: Order ORD123 is now SHIPPED
// SMS sent: Order ORD123 is now SHIPPED

orderService.updateOrderStatus('ORD123', 'DELIVERED');
// Email sent: Order ORD123 is now DELIVERED
// SMS sent: Order ORD123 is now DELIVERED
// Inventory updated for order ORD123

Node.js Built-in Observer: EventEmitter

Node.js has Observer Pattern built in via EventEmitter. This is the idiomatic Node.js approach.

import { EventEmitter } from 'events';

class OrderService extends EventEmitter {
updateOrderStatus(orderId, status) {
console.log(`Order ${orderId} status updated to ${status}`);
this.emit('ORDER_STATUS_CHANGED', { orderId, status });
}
}

Usage with EventEmitter

const orderService = new OrderService();

orderService.on('ORDER_STATUS_CHANGED', ({ orderId, status }) => {
console.log(`Email sent for order ${orderId} - status: ${status}`);
});

orderService.on('ORDER_STATUS_CHANGED', ({ orderId, status }) => {
console.log(`SMS sent for order ${orderId} - status: ${status}`);
});

orderService.on('ORDER_STATUS_CHANGED', ({ orderId, status }) => {
if (status === 'DELIVERED') {
console.log(`Inventory updated for ${orderId}`);
}
});

orderService.updateOrderStatus('ORD123', 'SHIPPED');

This is idiomatic Node.js — you are using Observer Pattern every time you call .on().


Advanced: Typed EventEmitter pattern

For large codebases, wrap EventEmitter to keep events organized:

import { EventEmitter } from 'events';

class OrderEventBus extends EventEmitter {
onOrderShipped(handler) {
this.on('ORDER_SHIPPED', handler);
return this; // chainable
}

onOrderDelivered(handler) {
this.on('ORDER_DELIVERED', handler);
return this;
}

emitOrderShipped(orderId) {
this.emit('ORDER_SHIPPED', { orderId, timestamp: Date.now() });
}

emitOrderDelivered(orderId) {
this.emit('ORDER_DELIVERED', { orderId, timestamp: Date.now() });
}
}

const bus = new OrderEventBus();

bus
.onOrderShipped(({ orderId }) => console.log(`Email: order ${orderId} shipped`))
.onOrderDelivered(({ orderId }) => console.log(`SMS: order ${orderId} delivered`));

bus.emitOrderShipped('ORD123');
bus.emitOrderDelivered('ORD123');

This gives you auto-complete friendly event names and removes magic strings from consumer code.


Real backend use cases

User signup triggers

import { EventEmitter } from 'events';

class UserService extends EventEmitter {
async register(userData) {
const user = await db.users.create(userData);
this.emit('USER_REGISTERED', user);
return user;
}
}

const userService = new UserService();

// Each concern registers its own handler — completely decoupled
userService.on('USER_REGISTERED', (user) => emailService.sendWelcome(user));
userService.on('USER_REGISTERED', (user) => trialService.createFreeTrial(user));
userService.on('USER_REGISTERED', (user) => crmService.addContact(user));
userService.on('USER_REGISTERED', (user) => analyticsService.track('signup', user));

The UserService doesn't know what happens after registration. Each service owns its reaction.


Inventory alert system

class InventoryService extends EventEmitter {
updateStock(productId, quantity) {
this.stock[productId] = quantity;

if (quantity < this.lowStockThreshold) {
this.emit('LOW_STOCK', { productId, quantity });
}
}
}

const inventory = new InventoryService();

inventory.on('LOW_STOCK', ({ productId }) => {
slackService.alert(`Low stock alert for product ${productId}`);
});

inventory.on('LOW_STOCK', ({ productId, quantity }) => {
procurementService.raiseOrder(productId, quantity);
});

WebSocket broadcast on DB change

class DataService extends EventEmitter {
async updateRecord(id, data) {
const updated = await db.update(id, data);
this.emit('RECORD_UPDATED', updated);
return updated;
}
}

const dataService = new DataService();

// Broadcast to all connected WebSocket clients
dataService.on('RECORD_UPDATED', (record) => {
wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify({ type: 'UPDATE', payload: record }));
}
});
});

One-time observers with .once()

// This handler fires only once, then auto-removes itself
orderService.once('ORDER_PLACED', (order) => {
console.log(`First order celebration email for ${order.id}`);
});

Useful for: onboarding flows, one-time payment confirmations, initialization hooks.


Removing observers

function auditHandler({ orderId, status }) {
auditLog.write({ orderId, status, at: new Date() });
}

orderService.on('ORDER_STATUS_CHANGED', auditHandler);

// Later — remove it
orderService.off('ORDER_STATUS_CHANGED', auditHandler);
// or:
orderService.removeListener('ORDER_STATUS_CHANGED', auditHandler);

Benefits

1. Loose Coupling

The subject doesn't know anything about its observers. Add or remove observers without touching the subject.


2. Open/Closed Principle

Add a new observer (e.g., PushNotificationObserver) without modifying existing code.


3. Event-driven architecture

Fits perfectly with Node.js's async, non-blocking event model.


When to use Observer Pattern

  • one change should trigger multiple independent reactions
  • you want to decouple the publisher from the subscribers
  • the number of subscribers can vary at runtime
  • you're building an event-driven or pub/sub architecture

When NOT to use it

  • when the chain of notifications is hard to trace (debugging becomes difficult)
  • when observers must be notified in a guaranteed order with tight coupling
  • for very simple one-to-one triggers — just call the function directly

Interview definition (short answer)

"Observer Pattern is a behavioral design pattern where a subject maintains a list of observers and notifies them automatically when its state changes, enabling loose coupling between the publisher and subscribers."


Best mental model

If you see code like:

sendEmail(order);
sendSms(order);
updateInventory(order);
updateAnalytics(order);

All tightly coupled inside one function — ask yourself:

"Should this become an event that observers react to?"

Formula:

Subject.notify(event) → [Observer1, Observer2, Observer3].update()